/*
 * Copyright 2002-2015 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.huihoo.ofbiz.smart.webapp;

import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import javax.servlet.FilterChain;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;

import org.huihoo.ofbiz.smart.session.ExpiringSession;
import org.huihoo.ofbiz.smart.session.Session;
import org.huihoo.ofbiz.smart.session.SessionRepository;

/**
 * Switches the {@link javax.servlet.http.HttpSession} implementation to be backed by a
 * {@link org.springframework.session.Session}.
 *
 * The {@link SessionRepositoryFilter} wraps the {@link javax.servlet.http.HttpServletRequest} and
 * overrides the methods to get an {@link javax.servlet.http.HttpSession} to be backed by a
 * {@link org.springframework.session.Session} returned by the
 * {@link org.springframework.session.SessionRepository}.
 *
 * The {@link SessionRepositoryFilter} uses a {@link HttpSessionStrategy} (default
 * {@link CookieHttpSessionStrategy} to bridge logic between an
 * {@link javax.servlet.http.HttpSession} and the {@link org.springframework.session.Session}
 * abstraction. Specifically:
 *
 * <ul>
 * <li>The session id is looked up using
 * {@link HttpSessionStrategy#getRequestedSessionId(javax.servlet.http.HttpServletRequest)}. The
 * default is to look in a cookie named SESSION.</li>
 * <li>The session id of newly created {@link org.springframework.session.ExpiringSession} is sent
 * to the client using
 * <li>The client is notified that the session id is no longer valid with
 * {@link HttpSessionStrategy#onInvalidateSession(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
 * </li>
 * </ul>
 *
 * <p>
 * The SessionRepositoryFilter must be placed before any Filter that access the HttpSession or that
 * might commit the response to ensure the session is overridden and persisted properly.
 * </p>
 *
 * @since 1.0
 * @author Rob Winch
 */
@SuppressWarnings("deprecation")
public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerRequestFilter {
  public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class.getName();

  public static final int DEFAULT_ORDER = Integer.MIN_VALUE + 50;

  private final SessionRepository<S> sessionRepository;

  private ServletContext servletContext;

  private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();

  /**
   * Creates a new instance
   *
   * @param sessionRepository the <code>SessionRepository</code> to use. Cannot be null.
   */
  public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    if (sessionRepository == null) {
      throw new IllegalArgumentException("sessionRepository cannot be null");
    }
    this.sessionRepository = sessionRepository;
  }

  /**
   * Sets the {@link HttpSessionStrategy} to be used. The default is a
   * {@link CookieHttpSessionStrategy}.
   *
   * @param httpSessionStrategy the {@link HttpSessionStrategy} to use. Cannot be null.
   */
  public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
    if (httpSessionStrategy == null) {
      throw new IllegalArgumentException("httpSessionStrategy cannot be null");
    }
    this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(httpSessionStrategy);
  }

  /**
   * Sets the {@link MultiHttpSessionStrategy} to be used. The default is a
   * {@link CookieHttpSessionStrategy}.
   *
   * @param httpSessionStrategy the {@link MultiHttpSessionStrategy} to use. Cannot be null.
   */
  public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {
    if (httpSessionStrategy == null) {
      throw new IllegalArgumentException("httpSessionStrategy cannot be null");
    }
    this.httpSessionStrategy = httpSessionStrategy;
  }

  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
          FilterChain filterChain) throws ServletException, IOException {
    request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository);

    SessionRepositoryRequestWrapper wrappedRequest =
            new SessionRepositoryRequestWrapper(request, response, servletContext);
    SessionRepositoryResponseWrapper wrappedResponse =
            new SessionRepositoryResponseWrapper(wrappedRequest, response);

    HttpServletRequest strategyRequest =
            httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);
    HttpServletResponse strategyResponse =
            httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);

    try {
      filterChain.doFilter(strategyRequest, strategyResponse);
    } finally {
      wrappedRequest.commitSession();
    }
  }

  public void setServletContext(ServletContext servletContext) {
    this.servletContext = servletContext;
  }

  /**
   * Allows ensuring that the session is saved if the response is committed.
   *
   * @author Rob Winch
   * @since 1.0
   */
  private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {

    private final SessionRepositoryRequestWrapper request;

    /**
     * @param response the response to be wrapped
     */
    public SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
            HttpServletResponse response) {
      super(response);
      if (request == null) {
        throw new IllegalArgumentException("request cannot be null");
      }
      this.request = request;
    }

    @Override
    protected void onResponseCommitted() {
      request.commitSession();
    }
  }

  /**
   * A {@link javax.servlet.http.HttpServletRequest} that retrieves the
   * {@link javax.servlet.http.HttpSession} using a
   * {@link org.springframework.session.SessionRepository}.
   *
   * @author Rob Winch
   * @since 1.0
   */
  private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
    private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class.getName();
    private Boolean requestedSessionIdValid;
    private boolean requestedSessionInvalidated;
    private final HttpServletResponse response;
    private final ServletContext servletContext;

    private SessionRepositoryRequestWrapper(HttpServletRequest request,
            HttpServletResponse response, ServletContext servletContext) {
      super(request);
      this.response = response;
      this.servletContext = servletContext;
    }

    /**
     * Uses the HttpSessionStrategy to write the session id tot he response and persist the Session.
     */
    private void commitSession() {
      HttpSessionWrapper wrappedSession = getCurrentSession();
      if (wrappedSession == null) {
        if (isInvalidateClientSession()) {
          httpSessionStrategy.onInvalidateSession(this, response);
        }
      } else {
        S session = wrappedSession.session;
        sessionRepository.save(session);
        if (!isRequestedSessionIdValid() || !session.getId().equals(getRequestedSessionId())) {
          httpSessionStrategy.onNewSession(session, this, response);
        }
      }
    }

    @SuppressWarnings("unchecked")
    private HttpSessionWrapper getCurrentSession() {
      return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
    }

    private void setCurrentSession(HttpSessionWrapper currentSession) {
      if (currentSession == null) {
        removeAttribute(CURRENT_SESSION_ATTR);
      } else {
        setAttribute(CURRENT_SESSION_ATTR, currentSession);
      }
    }

    @SuppressWarnings({"unused", "unchecked"})
    public String changeSessionId() {
      HttpSession session = getSession(false);

      if (session == null) {
        throw new IllegalStateException(
                "Cannot change session ID. There is no session associated with this request.");
      }

      // eagerly get session attributes in case implementation lazily loads them
      Map<String, Object> attrs = new HashMap<String, Object>();
      Enumeration<String> iAttrNames = session.getAttributeNames();
      while (iAttrNames.hasMoreElements()) {
        String attrName = iAttrNames.nextElement();
        Object value = session.getAttribute(attrName);

        attrs.put(attrName, value);
      }

      sessionRepository.delete(session.getId());
      HttpSessionWrapper original = getCurrentSession();
      setCurrentSession(null);

      HttpSession newSession = getSession();
      original.session = ((HttpSessionWrapper) newSession).session;

      newSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
      for (Map.Entry<String, Object> attr : attrs.entrySet()) {
        String attrName = attr.getKey();
        Object attrValue = attr.getValue();
        newSession.setAttribute(attrName, attrValue);
      }
      return newSession.getId();
    }

    public boolean isRequestedSessionIdValid() {
      if (requestedSessionIdValid == null) {
        String sessionId = getRequestedSessionId();
        S session = sessionId == null ? null : sessionRepository.getSession(sessionId);
        return isRequestedSessionIdValid(session);
      }

      return requestedSessionIdValid;
    }

    private boolean isRequestedSessionIdValid(S session) {
      if (requestedSessionIdValid == null) {
        requestedSessionIdValid = session != null;
      }
      return requestedSessionIdValid;
    }

    private boolean isInvalidateClientSession() {
      return getCurrentSession() == null && requestedSessionInvalidated;
    }

    @Override
    public HttpSession getSession(boolean create) {
      HttpSessionWrapper currentSession = getCurrentSession();
      if (currentSession != null) {
        return currentSession;
      }
      String requestedSessionId = getRequestedSessionId();
      if (requestedSessionId != null) {
        S session = sessionRepository.getSession(requestedSessionId);
        if (session != null) {
          this.requestedSessionIdValid = true;
          currentSession = new HttpSessionWrapper(session, getServletContext());
          currentSession.setNew(false);
          setCurrentSession(currentSession);
          return currentSession;
        }
      }
      if (!create) {
        return null;
      }
      S session = sessionRepository.createSession();
      currentSession = new HttpSessionWrapper(session, getServletContext());
      setCurrentSession(currentSession);
      return currentSession;
    }

    public ServletContext getServletContext() {
      if (servletContext != null) {
        return servletContext;
      }
      // Servlet 3.0+
      return super.getServletContext();
    }

    @Override
    public HttpSession getSession() {
      return getSession(true);
    }

    @Override
    public String getRequestedSessionId() {
      return httpSessionStrategy.getRequestedSessionId(this);
    }

    /**
     * Allows creating an HttpSession from a Session instance.
     *
     * @author Rob Winch
     * @since 1.0
     */
    private final class HttpSessionWrapper implements HttpSession {
      private S session;
      private final ServletContext servletContext;
      private boolean invalidated;
      private boolean old;

      public HttpSessionWrapper(S session, ServletContext servletContext) {
        this.session = session;
        this.servletContext = servletContext;
      }

      public long getCreationTime() {
        checkState();
        return session.getCreationTime();
      }

      public String getId() {
        return session.getId();
      }

      public long getLastAccessedTime() {
        checkState();
        return session.getLastAccessedTime();
      }

      public ServletContext getServletContext() {
        return servletContext;
      }

      public void setMaxInactiveInterval(int interval) {
        session.setMaxInactiveIntervalInSeconds(interval);
      }

      public int getMaxInactiveInterval() {
        return session.getMaxInactiveIntervalInSeconds();
      }

      public HttpSessionContext getSessionContext() {
        return NOOP_SESSION_CONTEXT;
      }

      public Object getAttribute(String name) {
        checkState();
        return session.getAttribute(name);
      }

      public Object getValue(String name) {
        return getAttribute(name);
      }

      public Enumeration<String> getAttributeNames() {
        checkState();
        return Collections.enumeration(session.getAttributeNames());
      }

      public String[] getValueNames() {
        checkState();
        Set<String> attrs = session.getAttributeNames();
        return attrs.toArray(new String[0]);
      }

      public void setAttribute(String name, Object value) {
        checkState();
        session.setAttribute(name, value);
      }

      public void putValue(String name, Object value) {
        setAttribute(name, value);
      }

      public void removeAttribute(String name) {
        checkState();
        session.removeAttribute(name);
      }

      public void removeValue(String name) {
        removeAttribute(name);
      }

      public void invalidate() {
        checkState();
        this.invalidated = true;
        requestedSessionInvalidated = true;
        setCurrentSession(null);
        sessionRepository.delete(getId());
      }

      public void setNew(boolean isNew) {
        this.old = !isNew;
      }

      public boolean isNew() {
        checkState();
        return !old;
      }

      private void checkState() {
        if (invalidated) {
          throw new IllegalStateException("The HttpSession has already be invalidated.");
        }
      }
    }
  }

  private static final HttpSessionContext NOOP_SESSION_CONTEXT = new HttpSessionContext() {
    public HttpSession getSession(String sessionId) {
      return null;
    }

    public Enumeration<String> getIds() {
      return EMPTY_ENUMERATION;
    }
  };

  private static final Enumeration<String> EMPTY_ENUMERATION = new Enumeration<String>() {
    public boolean hasMoreElements() {
      return false;
    }

    public String nextElement() {
      throw new NoSuchElementException("a");
    }
  };

  static class MultiHttpSessionStrategyAdapter implements MultiHttpSessionStrategy {
    private HttpSessionStrategy delegate;

    public MultiHttpSessionStrategyAdapter(HttpSessionStrategy delegate) {
      this.delegate = delegate;
    }

    public String getRequestedSessionId(HttpServletRequest request) {
      return delegate.getRequestedSessionId(request);
    }

    public void onNewSession(Session session, HttpServletRequest request,
            HttpServletResponse response) {
      delegate.onNewSession(session, request, response);
    }

    public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
      delegate.onInvalidateSession(request, response);
    }

    public HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response) {
      return request;
    }

    public HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response) {
      return response;
    }
  }
}
